#region File Header
// Copyright (c) 2011, Stefan Stolz
// All rights reserved.
// 
// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
// 
// * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
// 
// * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
// 
// * Neither the name of Sharp2City nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
// 
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#endregion
#region using directives

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Collections.Specialized;
using System.Text;

#endregion

namespace Sharp2City
{
    /// <summary>
    ///   The Client for the Team City REST API
    /// </summary>
    public sealed class TeamCityClient
    {
        #region private fields

        /// <summary>
        ///   The Configuration for the Team City Access
        /// </summary>
        private readonly ITeamCityAccessConfiguration accessConfiguration;

        /// <summary>
        ///   The <see cref = "TeamCityConnection" /> to use
        /// </summary>
        private readonly TeamCityConnection connection;

        #endregion

        #region constructors

        /// <summary>
        ///   Initializes a new instance of the <see cref = "TeamCityClient" /> class.
        /// </summary>
        /// <param name = "accessConfiguration">The access configuration.</param>
        public TeamCityClient(ITeamCityAccessConfiguration accessConfiguration)
        {
            if (accessConfiguration == null) throw new ArgumentNullException("accessConfiguration");
            this.accessConfiguration = accessConfiguration;

            this.connection = new TeamCityConnection(this.accessConfiguration);
        }

        /// <summary>
        ///   Initializes a new instance of the <see cref = "TeamCityClient" /> class.
        /// </summary>
        /// <param name = "hostName">Name of the host.</param>
        /// <param name = "userName">Name of the user.</param>
        /// <param name = "password">The password.</param>
        public TeamCityClient(string hostName, string userName, string password)
            : this(new DefaultAccessConfiguration(
                       hostName,
                       userName,
                       password,
                       false,
                       false))
        {
        }

        /// <summary>
        ///   Initializes a new instance of the <see cref = "TeamCityClient" /> class.
        /// </summary>
        /// <param name = "hostName">Name of the host.</param>
        /// <param name = "userName">Name of the user.</param>
        /// <param name = "password">The password.</param>
        /// <param name = "useSsl">A value indicating whether to use SSL or not</param>
        public TeamCityClient(string hostName, string userName, string password, bool useSsl)
            : this(new DefaultAccessConfiguration(
                       hostName,
                       userName,
                       password,
                       useSsl,
                       false))
        {
        }

        /// <summary>
        ///   Initializes a new instance of the <see cref = "TeamCityClient" /> class.
        /// </summary>
        /// <param name = "hostName">Name of the host.</param>
        /// <param name = "userName">Name of the user.</param>
        /// <param name = "password">The password.</param>
        /// <param name = "useSsl">A value indicating whether to use SSL or not</param>
        /// <param name="trustSslCertificate">if using SSL and the certificate is not valid (selfcert) then setting
        /// this to true will ignore any certificate errors that would normally abort the connection</param>
        public TeamCityClient(string hostName, string userName, string password, bool useSsl, bool trustSslCertificate)
            : this(new DefaultAccessConfiguration(
                       hostName,
                       userName,
                       password,
                       useSsl,
                       trustSslCertificate))
        {
        }

        #endregion

        #region public methods

        /// <summary>
        ///   Downloads the Artifacts of the Build with the specified <paramref name = "buildId" />
        /// </summary>
        /// <param name = "buildId">The Build Id of the Artifacts to load</param>
        /// <param name = "processArtifactsCallback">A method that will be called after the download.</param>
        public void DownloadArtifacts(int buildId, ProcessArtifactsCallback processArtifactsCallback)
        {
            Uri uri = this.connection.CreateUri(
                string.Format(
                    CultureInfo.InvariantCulture,
                    "/httpAuth/downloadArtifacts.html?buildId={0}",
                    buildId));

            string tempFileName = Path.GetTempFileName();
            try
            {
                this.connection.Download(
                    uri,
                    tempFileName);
                processArtifactsCallback(tempFileName);
            }
            finally
            {
                if (File.Exists(tempFileName)) File.Delete(tempFileName);
            }
        }

        ///<summary>
        ///</summary>
        ///<param name="buildInfo"></param>
        ///<param name="artifact"></param>
        ///<param name="processArtifactsCallback"></param>
        public void DownloadArtifact(
            TeamCityBuildBase buildInfo, string artifact, ProcessArtifactsCallback processArtifactsCallback)
        {
            Uri uri =
                this.connection.CreateUri(
                    string.Format(
                        CultureInfo.InvariantCulture,
                        "/httpAuth/repository/download/{0}/{1}:id/{2}",
                        buildInfo.BuildTypeId,
                        buildInfo.BuildId,
                        artifact));

            DownloadArtifactFromUri(uri, processArtifactsCallback);
        }

        ///<summary>
        ///</summary>
        ///<param name="buildId"></param>
        ///<param name="zipFile"></param>
        ///<param name="artifact"></param>
        ///<param name="processArtifactsCallback"></param>
        public void DownloadZippedArtifact(
            int buildId, string zipFile, string artifact, ProcessArtifactsCallback processArtifactsCallback)
        {
            Uri uri =
                this.connection.CreateUri(
                    string.Format(
                        CultureInfo.InvariantCulture,
                        "/httpAuth/repository/archive/{0}/buildId/{1}/{2}",
                        zipFile,
                        buildId,
                        artifact));

            DownloadArtifactFromUri(uri, processArtifactsCallback);
        }

        private void DownloadArtifactFromUri(Uri uri, ProcessArtifactsCallback processArtifactsCallback)
        {
            string tempFileName = Path.GetTempFileName();

            try
            {
                this.connection.Download(uri, tempFileName);
                processArtifactsCallback(tempFileName);
            }
            finally
            {
                if (File.Exists(tempFileName)) File.Delete(tempFileName);
            }
        }

        /// <summary>
        ///   Downloads the Build Log for the specified <paramref name = "buildId" />
        /// </summary>
        /// <param name = "buildId">The buildId of the Build to load the Build Log</param>
        /// <returns>A <see cref = "string" /> with the Build Log</returns>
        public string DownloadBuildLog(int buildId)
        {
            Uri uri = this.connection.CreateUri(
                string.Format(
                    CultureInfo.InvariantCulture,
                    "/httpAuth/downloadBuildLog.html?buildId={0}",
                    buildId));

            return (this.connection.RequestText(uri));
        }

        /// <summary>
        ///   Searches for the <see cref = "TeamCityBuildConfiguration" /> of the project <paramref name = "projectName" />
        ///   with the name <paramref name = "configurationName" />.
        /// </summary>
        /// <param name = "projectName">The name of the project</param>
        /// <param name = "configurationName">The name of the build configuration</param>
        /// <returns>An instance of <see cref = "TeamCityBuildConfiguration" /> if a Buildconfiguraiton for the specified names was found; otherwise <c>null</c></returns>
        public TeamCityBuildConfiguration FindBuildConfiguration(string projectName, string configurationName)
        {
            TeamCityProject teamCityProject = this.FindProject(projectName);

            if (teamCityProject != null)
            {
                return (this.FindBuildConfiguration(
                    teamCityProject,
                    configurationName));
            }

            return (null);
        }

        ///<summary>
        /// Return the name of the build configuration.
        ///</summary>
        ///<param name="buildTypeId"></param>
        ///<returns></returns>
        public string FindBuildConfigurationName(string buildTypeId)
        {
            foreach (TeamCityProject teamCityProject in this.GetAllProjects())
            {
                foreach (TeamCityBuildConfiguration teamCityBuildConfiguration in
                    this.GetBuildConfigurations(teamCityProject))
                {
                    if (teamCityBuildConfiguration.Id.Equals(buildTypeId, StringComparison.Ordinal))
                    {
                        return (teamCityBuildConfiguration.Name);
                    }
                }
            }

            return (null);
        }

        /// <summary>
        ///   Searches for the <see cref = "TeamCityBuildConfiguration" /> with in name <paramref name = "configurationName" /> in the project specified by <paramref name = " teamCityProject" />
        /// </summary>
        /// <param name = "teamCityProject">The project to look at</param>
        /// <param name = "configurationName">The name of the build configuration</param>
        /// <returns>An instance of <see cref = "TeamCityBuildConfiguration" /> if a Buildconfiguraiton for the specified names was found; otherwise <c>null</c></returns>
        public TeamCityBuildConfiguration FindBuildConfiguration(
            TeamCityProject teamCityProject,
            string configurationName)
        {
            foreach (
                TeamCityBuildConfiguration teamCityBuildConfiguration in
                    this.GetBuildConfigurations(teamCityProject))
            {
                if (teamCityBuildConfiguration.Name.Equals(
                    configurationName,
                    StringComparison.Ordinal))
                {
                    return (teamCityBuildConfiguration);
                }
            }

            return (null);
        }

        /// <summary>
        ///   Searches for the last successful Build in the <paramref name = "buildConfiguration" />
        /// </summary>
        /// <param name = "buildConfiguration">The Build configuration where to look</param>
        /// <returns>An instance of <see cref = "TeamCityBuild" /> if a sucessful Build was found; otherwise <c>null</c></returns>
        public TeamCityBuild FindLastSuccessfulBuild(TeamCityBuildConfiguration buildConfiguration)
        {
            foreach (TeamCityBuild teamCityBuild in this.GetBuilds(buildConfiguration))
            {
                if (teamCityBuild.BuildStatus
                    == BuildStatus.Success)
                {
                    return (teamCityBuild);
                }
            }

            return (null);
        }

        ///<summary>
        /// Returns the last build, regardless of status
        ///</summary>
        ///<param name="buildConfiguration"></param>
        ///<returns></returns>
        public TeamCityBuild GetLastBuild(TeamCityBuildConfiguration buildConfiguration)
        {
            IEnumerable<TeamCityBuild> results = this.GetBuilds(buildConfiguration);

            return results == null ? null : results.First();
        }

        /// <summary>
        ///   Searches for the project with specified name
        /// </summary>
        /// <param name = "projectName">The name of the Project to search for</param>
        /// <returns>An instance of <see cref = "TeamCityProject" /> if a Project was found; otherwise <c>null</c></returns>
        public TeamCityProject FindProject(string projectName)
        {
            foreach (TeamCityProject teamCityProject in this.GetAllProjects())
            {
                if (teamCityProject.Name.Equals(
                    projectName,
                    StringComparison.Ordinal))
                {
                    return (teamCityProject);
                }
            }

            return (null);
        }

        /// <summary>
        ///   Returns a Collection of all Builds of the TeamCity Server
        /// </summary>
        /// <returns></returns>
        public IEnumerable<TeamCityBuild> GetAllBuilds()
        {
            return (this.GetBuilds("/httpAuth/app/rest/builds"));
        }

        ///<summary>
        /// Returns the last successful build for a build type id
        ///</summary>
        ///<param name="buildType"></param>
        ///<returns></returns>
        public TeamCityBuild GetLastSuccessfulBuild(string buildType)
        {
            return this.GetBuilds(
                string.Format(
                    CultureInfo.InvariantCulture,
                    "/httpAuth/app/rest/buildTypes/{0}/builds?status={1}&count=1",
                    buildType,
                    BuildStatus.Success.ToString().ToUpper())
                ).FirstOrDefault();
        }

        /// <summary>
        ///   Returns a Collection of all TeamCity Projects
        /// </summary>
        /// <returns>Collection of <see cref = "TeamCityProject" /></returns>
        public IEnumerable<TeamCityProject> GetAllProjects()
        {
            Uri uri = this.connection.CreateUri("/httpAuth/app/rest/projects");

            XmlProjects projectsElement = this.connection.Request<XmlProjects>(uri);

            foreach (XmlProject projectInfo in projectsElement.Projects)
            {
                Uri projectInfoUri = this.connection.CreateUri(projectInfo.Href);

                XmlProject project = this.connection.Request<XmlProject>(projectInfoUri);

                yield return new TeamCityProject(project);
            }
        }

        /// <summary>
        ///   Returns a Collection of all TeamCity Build Configuartions of the specified <paramref name = "project" />
        /// </summary>
        /// <param name = "project">The Project</param>
        /// <returns>A Collection of <see cref = "TeamCityBuildConfiguration" /></returns>
        public IEnumerable<TeamCityBuildConfiguration> GetBuildConfigurations(TeamCityProject project)
        {
            Uri uri = this.connection.CreateUri(project.Href);

            XmlProject xmlProject = this.connection.Request<XmlProject>(uri);

            foreach (XmlBuildType xmlBuildType in xmlProject.BuildType.BuildType)
            {
                Uri buildTypeUri = this.connection.CreateUri(xmlBuildType.Href);

                XmlDetailedBuildType detailedBuildType = this.connection.Request<XmlDetailedBuildType>(buildTypeUri);

                yield return new TeamCityBuildConfiguration(detailedBuildType);
            }
        }

        /// <summary>
        ///   Returns a Collection of all TeamCity Builds of the specified <paramref name = "configuration" />
        /// </summary>
        /// <param name = "configuration">The Configuration</param>
        /// <returns>A Collection of <see cref = "TeamCityBuildConfiguration" /></returns>
        public IEnumerable<TeamCityBuild> GetBuilds(TeamCityBuildConfiguration configuration)
        {
            string relativeUrl = configuration.BuildsUrl;

            return (this.GetBuilds(relativeUrl));
        }

        /// <summary>
        ///   Returns a Collection of all TeamCity VcsRoots
        /// </summary>
        /// <returns>Collection of <see cref = "TeamCityVcsRoot" /></returns>
        public IEnumerable<TeamCityVcsRoot> GetAllVcsRoots()
        {
            Uri uri = this.connection.CreateUri("/httpAuth/app/rest/vcs-roots");

            XmlVcsRoots vcsRoots = this.connection.Request<XmlVcsRoots>(uri);

            foreach (var vcsRootBasic in vcsRoots.VcsRoots)
            {
                //TODO: This is probably a bug cannot access the vcsroot with Href have to cut "/id:"
                Uri vcsRootDetailUri = this.connection.CreateUri(vcsRootBasic.Href.Replace(@"/id:", @"/"));
                //Uri vcsRootDetailUri = this.connection.CreateUri(vcsRootBasic.Href);

                XmlVcsRootDetail vcsRootDetail = this.connection.Request<XmlVcsRootDetail>(vcsRootDetailUri);

                yield return new TeamCityVcsRoot(vcsRootDetail);
            }
        }

        /// <summary>
        ///   Returns a Collection of all TeamCity Users
        /// </summary>
        /// <returns>Collection of <see cref = "TeamCityUser" /></returns>
        public IEnumerable<TeamCityUser> GetAllUsers()
        {
            Uri uri = this.connection.CreateUri("/httpAuth/app/rest/users");

            XmlUsers users = this.connection.Request<XmlUsers>(uri);

            foreach (var user in users.Users)
            {
                Uri userDetailUri = this.connection.CreateUri(user.Href);

                XmlUserDetail userDetail = this.connection.Request<XmlUserDetail>(userDetailUri);

                yield return new TeamCityUser(userDetail);
            }
        }

        /// <summary>
        ///   Returns a Collection of all TeamCity Groups
        /// </summary>
        /// <returns>Collection of <see cref = "TeamCityGroup" /></returns>
        public IEnumerable<TeamCityGroup> GetAllGroups()
        {
            Uri uri = this.connection.CreateUri("/httpAuth/app/rest/userGroups");

            XmlGroups groups = this.connection.Request<XmlGroups>(uri);

            foreach (var group in groups.Groups)
            {
                Uri groupDetailUri = this.connection.CreateUri(group.Href);

                XmlGroupDetail groupDetail = this.connection.Request<XmlGroupDetail>(groupDetailUri);

                yield return new TeamCityGroup(groupDetail);
            }
        }      
        
        /// <summary>
        /// Set a pin for a specific build
        /// </summary>
        /// <param name="buildId">build id</param>
        /// <param name="pinComment">PinInfo comment</param>
        public void PinBuild(int buildId, string pinComment)
        {
            //httpAuth/ajax.html?pin=true&buildId=6654&pinComment=NoComment
            Uri uri = this.connection.CreateUri("httpAuth/ajax.html");
            NameValueCollection parameter = new NameValueCollection();

            parameter.Add("pin", "true");
            parameter.Add("buildId", buildId.ToString(CultureInfo.InvariantCulture));
            parameter.Add("pinComment", pinComment);               
        
            this.connection.Post(uri, parameter);            
        }

        /// <summary>
        /// Remove a pin for a specific build
        /// </summary>
        /// <param name="buildId">build id</param>
        public void UnPinBuild(int buildId)
        {
            Uri uri = this.connection.CreateUri("httpAuth/ajax.html");
            NameValueCollection parameter = new NameValueCollection();

            parameter.Add("pin", "false");
            parameter.Add("buildId", buildId.ToString(CultureInfo.InvariantCulture));

            this.connection.Post(uri, parameter);
        }

        /// <summary>
        /// Set a comment for a specific build
        /// </summary>
        /// <param name="buildId">build id</param>
        /// <param name="comment">Build comment</param>
        public void EditComment(int buildId, string comment)
        {
            ///httpAuth/ajax.html?buildComment=HelloWorld&buildId=6654
            Uri uri = this.connection.CreateUri("httpAuth/ajax.html");
            NameValueCollection parameter = new NameValueCollection();

            parameter.Add("buildId", buildId.ToString(CultureInfo.InvariantCulture));
            parameter.Add("buildComment", comment);

            this.connection.Post(uri, parameter);
        }

        /// <summary>
        /// Remove a comment for a specific build
        /// </summary>
        /// <param name="buildId">build id</param>
        public void RemoveComment(int buildId)
        {
            this.EditComment(buildId, string.Empty);
        }

        /// <summary>
        /// Set tags for a specific build.
        /// </summary>
        /// <remarks>
        /// Tags can only be a simple string. Space is here the delemiter.
        /// All existing tags will be deletet.
        /// </remarks>
        /// <param name="buildId">build id</param>
        /// <param name="tags">Collection with tags</param>
        public void EditTag(int buildId, params string[] tags)
        {
            //httpAuth/ajax.html?buildTagsInfo=TagOne&editTagsForBuild=6654
            Uri uri = this.connection.CreateUri("httpAuth/ajax.html");
            NameValueCollection parameter = new NameValueCollection();
            StringBuilder tagBuilder = new StringBuilder();

            parameter.Add("editTagsForBuild", buildId.ToString(CultureInfo.InvariantCulture));
            if (tags != null)
            {
                foreach (var tag in tags)
                {
                    tagBuilder.Append(tag);
                    tagBuilder.Append(" ");
                }
            }
            parameter.Add("buildTagsInfo", tagBuilder.ToString().Trim());

            this.connection.Post(uri, parameter);
        }

        /// <summary>
        /// Remove all tags for a specific build.
        /// </summary>
        /// <param name="buildId"></param>
        public void RemoveTags(int buildId)
        {
            this.EditTag(buildId);
        }

        #endregion

        #region public properties

        /// <summary>
        ///   Gets the <see cref = "ServerInfo" /> of the configured Host
        /// </summary>
        public ServerInfo ServerInfo
        {
            [DebuggerStepThrough]
            get
            {
                Uri serverInfoUri = this.connection.CreateUri("/httpAuth/app/rest/server");

                XmlServer xmlServer = this.connection.Request<XmlServer>(serverInfoUri);

                return (new ServerInfo(xmlServer));
            }
        }

        #endregion

        #region private methods

        /// <summary>
        ///   Returns a Collection of <see cref = "TeamCityBuild" /> for the specified Url
        /// </summary>
        /// <param name = "relativeUrl">The URL relative to the Host root</param>
        /// <returns>A Collection of <see cref = "TeamCityBuild" /></returns>
        private IEnumerable<TeamCityBuild> GetBuilds(string relativeUrl)
        {
            XmlBuildsCollection builds;
            do
            {
                Uri uri = this.connection.CreateUri(relativeUrl);
                builds = this.connection.Request<XmlBuildsCollection>(uri);

                foreach (XmlBuildBasic xmlBuild in builds.Build)
                {
                    Uri detailedUri = this.connection.CreateUri(xmlBuild.Href);

                    XmlBuildDetailed detailed = this.connection.Request<XmlBuildDetailed>(detailedUri);

                    yield return new TeamCityBuild(detailed);
                }

                relativeUrl = builds.NextHref;
            } while (!string.IsNullOrEmpty(builds.NextHref));
        }

        #endregion

        #region Nested type: DefaultAccessConfiguration

        /// <summary>
        ///   Implements the <see cref = "ITeamCityAccessConfiguration" /> with one time configured values
        /// </summary>
        private sealed class DefaultAccessConfiguration : ITeamCityAccessConfiguration
        {
            #region constructors

            /// <summary>
            ///   Initializes a new instance of the <see cref = "DefaultAccessConfiguration" /> class.
            /// </summary>
            /// <param name = "hostName">The Hostname</param>
            /// <param name = "userName">The Username</param>
            /// <param name = "password">The Password</param>
            /// <param name = "useSsl">A value indicating whether to use SSL or not</param>
            /// <param name="trustSslCertificate">if using SSL and the certificate is not valid (selfcert) then setting
            /// this to true will ignore any certificate errors that would normally abort the connection</param>
            public DefaultAccessConfiguration(string hostName, string userName, string password, bool useSsl, bool trustSslCertificate)
            {
                this.HostName = hostName;
                this.Password = password;
                this.UserName = userName;
                this.UseSSL = useSsl;
                this.TrustSslCertificate = trustSslCertificate;

                // always put a trailing slash here to lock in any path
                // supplied as part of the host
                if (!this.HostName.EndsWith("/"))
                    this.HostName += "/";
            }
            #endregion

            #region ITeamCityAccessConfiguration Members

            /// <summary>
            ///   Gets the Hostname
            /// </summary>
            public string HostName { get; private set; }

            /// <summary>
            ///   Gets the Password
            /// </summary>
            public string Password { get; private set; }

            /// <summary>
            ///   Gets the Username
            /// </summary>
            public string UserName { get; private set; }

            public bool UseSSL { get; private set; }

            public bool TrustSslCertificate { get; private set; }
            #endregion
        }

        #endregion
    }
}